package com.zzw.utils;


import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.SheetUtil;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import sun.misc.BASE64Encoder;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.LinkedHashMap;
import java.util.List;

public class ExcelUtil {

    /**
     * 导出Excel文件，支持指定表头与数据字段的映射关系
     * @param response HttpServletResponse对象，用于返回生成的Excel文件
     * @param headerFieldMap 表头与字段的映射关系，其中key为字段名，value为表头名
     * @param records 数据记录列表，其中每个对象表示一行数据
     * @param fileName 导出的文件名
     * @param sheetName 工作表的名称
     * @param headerStyle 表头的样式，可以为空，为空时使用默认样式
     * @param cellStyle 统一的单元格样式（字符串、数字、日期），可以为空，为空时使用默认样式
     * @throws IOException 如果在写入文件时发生IO错误
     */
    public static <T> void exportExcel(HttpServletRequest request,
                                       HttpServletResponse response, LinkedHashMap<String, String> headerFieldMap, List<T> records, String fileName, String sheetName, CellStyle headerStyle, CellStyle cellStyle) throws IOException {
        // 创建一个新的工作簿（Workbook），XSSFWorkbook用于生成Excel的.xlsx文件
        Workbook workbook = new XSSFWorkbook();

        // 在工作簿中创建一个新的工作表（Sheet），使用传入的sheetName作为表名
        Sheet sheet = workbook.createSheet(sheetName != null ? sheetName : "Sheet1"); // 如果sheetName为空，使用默认名称"Sheet1"

        // 如果headerStyle为空，则使用默认的表头样式
        headerStyle = headerStyle != null ? headerStyle : createDefaultHeaderStyle(workbook);
        // 如果cellStyle为空，则使用默认的单元格样式
        cellStyle = cellStyle != null ? cellStyle : createDefaultCellStyle(workbook);

        // 创建表头行，并设置样式
        createHeaderRow(sheet, headerFieldMap, headerStyle);

        // 填充数据行，使用统一的单元格样式
        fillDataRows(sheet, headerFieldMap, records, cellStyle);

        // 调整列宽，使列宽根据内容自动调整，防止表头被截断
        adjustColumnWidth(sheet);

        // 设置HTTP响应的内容类型为Excel文件类型
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        //给http响应,设置Content-Disposition
        //  这里考虑的细节比较多, 比如不同的浏览器写法不一样, 要考虑编码
        //  ff: 文件名中文需要base64, 而ie/chrome是URL编码
        //(1)如果是Firefox 中文编码需要base64
        //(2)Content-Disposition 指定下载的数据的展示形式(如果是attachment, 则使用文件下载方式;如果没有指定, 一般是以网页形式展示)
        //(3)如果是其它(主流ie/chrome), 中文编码使用URL编码
        if (request.getHeader("User-Agent").contains("Firefox")) {
            // 火狐 Base64编码
            response.setHeader("Content-Disposition", "attachment; filename==?UTF-8?B?" +
                    new BASE64Encoder().encode(fileName.getBytes("UTF-8")) + "?=");
        } else {
            // 其他(主流ie/chrome)使用URL编码操作
            response.setHeader("Content-Disposition", "attachment; filename=" +
                    URLEncoder.encode(fileName, "UTF-8"));
        }

        // 将生成的Excel文件写入HTTP响应的输出流中，供用户下载
        workbook.write(response.getOutputStream());

        // 关闭工作簿以释放资源
        workbook.close();
    }

    /**
     * 重载方法，支持指定表头与数据字段的映射关系，使用默认样式导出Excel文件，并动态指定Sheet名称
     * 方便用户在不需要自定义样式时使用
     * @param response HttpServletResponse对象，用于返回生成的Excel文件
     * @param headerFieldMap 表头与字段的映射关系，其中key为字段名，value为表头名
     * @param records 数据记录列表，其中每个对象表示一行数据
     * @param fileName 导出的文件名
     * @param sheetName 动态指定的工作表名称
     * @throws IOException 如果在写入文件时发生IO错误
     */
    public static <T> void exportExcel(HttpServletRequest request, HttpServletResponse response, LinkedHashMap<String, String> headerFieldMap, List<T> records, String fileName, String sheetName) throws IOException {
        // 调用主方法，并使用默认的单元格样式
        exportExcel(request, response, headerFieldMap, records, fileName, sheetName, null, null);
    }

    /**
     * 根据指定的表头与字段的映射关系创建Excel表头行
     * @param sheet Excel表单对象，表示当前的工作表
     * @param headerFieldMap 表头与字段的映射关系，其中key为字段名，value为表头名
     * @param headerStyle 表头单元格样式
     */
    private static void createHeaderRow(Sheet sheet, LinkedHashMap<String, String> headerFieldMap, CellStyle headerStyle) {
        // 创建工作表中的第一行，行号为0，表示表头行
        Row headerRow = sheet.createRow(0);

        // 初始化列号
        int colIdx = 0;

        // 遍历headerFieldMap，为每个表头创建单元格
        for (String fieldName : headerFieldMap.keySet()) {
            // 创建一个新的单元格，列号为colIdx
            Cell cell = headerRow.createCell(colIdx++);
            // 设置单元格的内容为表头名
            cell.setCellValue(headerFieldMap.get(fieldName));
            // 为单元格应用表头样式
            cell.setCellStyle(headerStyle);
        }
    }

    /**
     * 填充Excel数据行
     * 使用反射自动填充数据行，适用于任何类型的数据对象，并支持选择性导出指定字段
     * @param sheet Excel表单对象，表示当前的工作表
     * @param headerFieldMap 表头与字段的映射关系，其中key为字段名，value为表头名
     * @param records 数据记录列表，其中每个对象表示一行数据
     * @param cellStyle 统一的单元格样式（字符串、数字、日期）
     */
    private static <T> void fillDataRows(Sheet sheet, LinkedHashMap<String, String> headerFieldMap, List<T> records, CellStyle cellStyle) {
        // 创建一个日期格式化工具，用于将日期对象格式化为字符串
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // 初始化行号，从1开始（0是表头）
        int rowIdx = 1;

        // 遍历记录列表，为每个记录创建一行数据
        for (T record : records) {
            // 在工作表中创建新的一行，行号为rowIdx
            Row row = sheet.createRow(rowIdx++);

            // 初始化列号，从0开始
            int colIdx = 0;

            // 遍历headerFieldMap，为每个指定的字段创建一个单元格
            for (String fieldName : headerFieldMap.keySet()) {
                try {
                    // 获取当前字段
                    Field field = record.getClass().getDeclaredField(fieldName);
                    // 设置可以访问私有字段
                    field.setAccessible(true);

                    // 在当前行中创建新单元格，列号为colIdx
                    Cell cell = row.createCell(colIdx++);

                    // 获取当前字段的值
                    Object value = field.get(record);

                    // 根据字段类型设置单元格的值，并使用统一的样式
                    if (value != null) {
                        if (value instanceof String) {
                            cell.setCellValue((String) value);
                        } else if (value instanceof Integer || value instanceof Long || value instanceof Double) {
                            cell.setCellValue(Double.parseDouble(value.toString()));
                        } else if (value instanceof java.util.Date) {
                            // 如果字段是日期类型，使用格式化工具将其转换为字符串
                            cell.setCellValue(sdf.format(value));
                        } else {
                            // 对于其他类型，使用toString方法转换为字符串
                            cell.setCellValue(value.toString());
                        }
                        // 为所有单元格应用统一的样式
                        cell.setCellStyle(cellStyle);
                    }
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    // 如果无法访问字段（理论上不应该发生），设置单元格内容为"ERROR"
                    Cell cell = row.createCell(colIdx++);
                    cell.setCellValue("ERROR");
                    cell.setCellStyle(cellStyle); // 也为错误信息单元格应用样式
                }
            }
        }
    }

    /**
     * 调整列宽，确保表头不会被截断
     * @param sheet Excel表单对象，表示当前的工作表
     */
    private static void adjustColumnWidth(Sheet sheet) {
        // 获取表头行
        Row headerRow = sheet.getRow(0);

        // 遍历表头的每一列
        for (int i = 0; i < headerRow.getLastCellNum(); i++) {
            // 使用SheetUtil获取列的宽度，根据内容计算
            int columnWidth = (int) (SheetUtil.getColumnWidth(sheet, i, false) * 256);

            // 计算表头内容的宽度，getBytes().length * 256得到列宽
            int headerWidth = headerRow.getCell(i).getStringCellValue().getBytes().length * 256;

            // 如果表头宽度大于内容宽度，使用表头宽度
            if (headerWidth > columnWidth) {
                sheet.setColumnWidth(i, headerWidth);
            } else {
                // 否则，根据内容自适应宽度
                sheet.autoSizeColumn(i);
            }
        }
    }

    /**
     * 创建默认的表头单元格样式
     * @param workbook Excel工作簿对象，表示当前的工作簿
     * @return 表头单元格样式对象
     */
    private static CellStyle createDefaultHeaderStyle(Workbook workbook) {
        // 创建单元格样式对象
        CellStyle style = workbook.createCellStyle();

        // 设置单元格背景颜色为灰色
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);

        // 创建字体对象，并设置字体样式
        Font font = workbook.createFont();
        font.setBold(true); // 设置字体加粗
        font.setColor(IndexedColors.BLACK.getIndex()); // 设置字体颜色为黑色
        font.setFontHeightInPoints((short) 12); // 设置字体大小为12磅
        style.setFont(font); // 将字体应用到样式中

        // 设置单元格内容居中对齐
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);

        // 设置单元格的边框样式
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);

        // 返回配置好的样式
        return style;
    }

    /**
     * 创建默认的单元格样式，适用于字符串、数字和日期
     * @param workbook Excel工作簿对象，表示当前的工作簿
     * @return 单元格样式对象
     */
    private static CellStyle createDefaultCellStyle(Workbook workbook) {
        // 创建单元格样式对象
        CellStyle style = workbook.createCellStyle();

        // 设置单元格内容居中对齐
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);

        // 设置单元格的边框样式
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);

        // 返回配置好的样式
        return style;
    }

    public static void main(String[] args) {
        System.out.println(toRFC5987Filename("赵志伟"));
    }

    public static String toRFC5987Filename(String fileName) {
        try {
            byte[] bytes = fileName.getBytes(StandardCharsets.UTF_8);
            StringBuilder encoded = new StringBuilder();

            for (byte b : bytes) {
                // RFC 5987允许的未编码字符集
                if ((b >= '0' && b <= '9') ||
                        (b >= 'a' && b <= 'z') ||
                        (b >= 'A' && b <= 'Z') ||
                        b == '-' || b == '.' || b == '_' || b == '~') {
                    encoded.append((char)  b);
                } else {
                    encoded.append('%')
                            .append(String.format("%02X",  b & 0xFF));
                }
            }
            return encoded.toString();
        } catch (Exception e) {
            throw new IllegalArgumentException("Filename encoding failed", e);
        }
    }
}
